package org.hawk.gwt.ppc;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hawk.gwt.ppc.javacc.JavaScanner;
import org.hawk.gwt.ppc.javacc.ParseException;
import org.hawk.gwt.ppc.scanner.FileSystemSourcesLoader;
import org.hawk.gwt.ppc.scanner.ProcessingUnit;
import org.hawk.gwt.ppc.scanner.Processor;
import org.hawk.gwt.ppc.scanner.SourcesLoader;
import org.hawk.gwt.ppc.scanner.UniversalSourcesLoader;
import org.hawk.gwt.ppc.utils.FileSystem;
import org.hawk.gwt.ppc.utils.Name;

/**
 * Facade class to do all the preprocessing jobs.
 * Delegates all work to corresponding classes.
 * 
 * @author alex.bereznevatiy@gmail.com
 */
public class ProcessingUnitsManager extends JavaScanner {
	private static final String GWT_MODULE_EXTENSION = ".gwt.xml";
	private static final String WAR_DIR_PROPERTY_NAME = "-war";
	private static final String PPC_WORKING_DIR = "gwt-ppc";
	private static ProcessingUnitsManager instance;
	private ProcessingUnitsManager(){}
	
	private Environment env = Environment.getInstance();
	private Configuration conf;
	private File work;
	private UniversalSourcesLoader loader;
	private FileSystemSourcesLoader workLoader;
	private final LinkedList<ProcessingUnit> processors = new LinkedList<ProcessingUnit>();
	private Set<String> loadedModules;
	private Map<Processor,ProcessingUnit> processorsAssignment 
		= new HashMap<Processor, ProcessingUnit>();
	private String currentResource;

	public static ProcessingUnitsManager getInstance() {
		if(instance==null){
			instance = new ProcessingUnitsManager();
		}
		return instance;
	}
	/**
	 * @return name of current resource to process.
	 * Name is represented as java name.
	 */
	public String getCurrentResource(){
		return currentResource;
	}
	/**
	 * Adds specific preprocessor to this manager.
	 * @param preprocessor to add
	 */
	void addProcessingUnit(ProcessingUnit ppc) {
		processors.add(ppc);
		ppc.configure(conf, this);
	}
	/**
	 * Invokes actual preprocessing of java code for 
	 * the whole classpath.
	 * @throws ParseException
	 * @throws IOException 
	 */
	void run() throws ParseException, IOException {
		Log.info("Preprocessing java code");
		loadedModules = new HashSet<String>();
		iterateModules(env.getModules());
		Log.info("Preprocessing has been finished");
	}
	/**
	 * @return loader for all the processed sources.
	 */
	SourcesLoader getWorkLoader() {
		return workLoader;
	}
	/**
	 * Releases all resources taken from the system.
	 */
	void dispose() {
		FileSystem.markForDeletion(work);
	}
	/**
	 * Configures {@link ProcessingUnitsManager} to be able to run.
	 * @throws IOException
	 */
	void configure() throws IOException {
		conf = env.getConfiguration();
		createWorkDir();
		loader = new UniversalSourcesLoader();
		workLoader = new FileSystemSourcesLoader(work);
	}
	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.javacc.JavaScanner#addProcessor(int, org.hawk.gwt.ppc.scanner.Processor)
	 */
	@Override
	public void addProcessor(int nodeType, Processor processor) {
		super.addProcessor(nodeType, processor);
		if(processorsAssignment.get(processor)!=null&&
			!processorsAssignment.get(processor)
				.equals(processors.getLast())){
			throw new IllegalStateException("The same instance of "+
				Processor.class+" is assigned to more than one instance of "+
				ProcessingUnit.class);
		}
		processorsAssignment.put(processor, processors.getLast());
	}
	
	@Override
	protected List<Processor> getPreprocessorsForKind(int kind) {
		List<Processor> result = new LinkedList<Processor>();
		result.addAll(super.getPreprocessorsForKind(kind));
		Iterator<Processor> iterator = result.iterator();
		while(iterator.hasNext()){
			if(!processorsAssignment.get(iterator.next())
					.acceptResource(currentResource)){
				iterator.remove();
			}
		}
		return result;
	}
	private void createWorkDir() {
		String war = conf.get(WAR_DIR_PROPERTY_NAME,
				System.getProperty("java.io.tmpdir"));
		if(FileSystem.exists(war, PPC_WORKING_DIR)){
			FileSystem.rm(war,PPC_WORKING_DIR);
		}
		work = FileSystem.mkdir(war,PPC_WORKING_DIR);
	}
	
	private void iterateModules(String[] modules) 
			throws IOException, ParseException {
		for(String name : modules){
			processModule(name);
		}
	}
	
	void processModule(String name) throws IOException, ParseException {
		if(loadedModules==null||loadedModules.contains(name)){
			return;
		}
		loadedModules.add(name);
		URL moduleUrl = loader.getResource(
				Name.javaToFs(name, GWT_MODULE_EXTENSION));
		if(moduleUrl==null){
			throw new FileNotFoundException(
				"Unable to find descriptor for module: "+name);
		}
		GWTModule module = new GWTModule(name,moduleUrl);
		iterateModules(module.getInheritedModules());
		processModule(module);
		
	}
	
	private void processModule(GWTModule module) 
			throws IOException, ParseException {
		if(!hasAtLeastOneResourceToPreprocess(module)){
			Log.debug("\tNothing to process for module: "+module);
			return;
		}
		Log.info("   Processing module "+module.getName());
		processModule(module.getClientPackages());
		processEntryPoints(module);
		processModuleDescriptor(module);
	}
	
	private void processEntryPoints(GWTModule module) 
			throws IOException {
		for(String entryPoint : module.getEntryPoints()){
			processEntryPoint(entryPoint);
		}
	}
	
	private void processEntryPoint(String entryPoint) 
			throws IOException {
		if(processors.isEmpty()){
			return;
		}
		Reader reader = new InputStreamReader(
				workLoader.getClass(entryPoint).openStream());
		try {
			for(ProcessingUnit ppc : processors){
				StringWriter writer = new StringWriter();
				ppc.processEntryPoint(entryPoint,reader,writer);
				reader.close();
				reader = new StringReader(writer.toString());
			}
			FileSystem.write(new File(work,
				Name.javaToFs(entryPoint)),reader);
		}
		finally{
			reader.close();
		}
	}
	
	private void processModuleDescriptor(GWTModule module) 
			throws IOException, ParseException {
		for(ProcessingUnit ppc : processors){
			ppc.processModule(module);
		}
		FileSystem.write(new File(work,
			Name.javaToFs(module.getName(),GWT_MODULE_EXTENSION)),
			module.toStream());
	}
	
	private void processModule(String[] clientPackages) 
			throws IOException, ParseException {
		for(String pkg : clientPackages){
			processPackage(pkg);
		}
	}
	
	private void processPackage(String pkg) 
			throws IOException, ParseException {
		for(String clazz : loader.getClasses(pkg)){
			processClass(clazz);
		}
		processModule(loader.getSubpackages(pkg).toArray(new String[0]));
	}
	
	private void processClass(String clazz) 
			throws IOException, ParseException {
		currentResource = clazz;
		Reader reader = new InputStreamReader(
				loader.getClass(clazz).openStream());
		try {
			StringWriter writer = new StringWriter();
			startScan(reader, writer);
			FileSystem.write(new File(work,
				Name.javaToFs(clazz)),
				new StringReader(writer.toString()));
		}
		finally{
			currentResource = null;
			reader.close();
		}
	}
	
	private boolean canBeProcessed(String resource) {
		for(ProcessingUnit p : processors){
			if(p.acceptResource(resource)){
				return true;
			}
		}
		return false;
	}
	
	private boolean hasAtLeastOneResourceToPreprocess(GWTModule module) {
		return hasAtLeastOneResourceToPreprocess(module.getClientPackages());
	}
	
	private boolean hasAtLeastOneResourceToPreprocess(String[] packages){
		for(String pkg : packages){
			if(hasAtLeastOneResourceToPreprocess(pkg)||
				hasAtLeastOneResourceToPreprocess(
						loader.getSubpackages(pkg).toArray(new String[0]))){
				return true;
			}
		}
		return false;
	}
	
	private boolean hasAtLeastOneResourceToPreprocess(String pkg) {
		for(String cls : loader.getClasses(pkg)){
			if(canBeProcessed(cls)){
				return true;
			}
		}
		return false;
	}
}
